-module(router).
-behaviour(gen_server).

-export([start_link/0]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
     terminate/2, code_change/3, comet_loop/1]).

-export([send/3, get_presence/2, login/2, logout/1, get_subscribers/2]).

-define(SERVER, global:whereis_name(?MODULE)).
-define(REDIS_HOST, "127.0.0.1").
-define(REDIS_PORT, 8379).
-define(MONGO_HOST, "192.168.51.103").
-define(MONGO_PORT, 6011).
-define(UC_HOST, "192.168.51.103").
-define(UC_PORT, 22000).
-define(LOGINSET, "login").

% will hold bidirectional mapping between id <--> pid
-record(state, {sid2comet,comet2sid, pid2id, id2pid, redisconn, mongoconn,ucconn}).

start_link() ->
    gen_server:start_link({global, ?MODULE}, ?MODULE, [], []).

% sends Msg to anyone logged in as Id
send(Id,From, Msg) ->
    gen_server:call(?SERVER, {send, Id,From, Msg}).

login({Sid, Id,Pwd}, Pid) when is_pid(Pid) ->
    gen_server:call(?SERVER, {login, {Sid, Id, Pwd}, Pid}).

logout(Pid) when is_pid(Pid) ->
    gen_server:call(?SERVER, {logout, Pid}).

get_presence(From, Pid) ->
    gen_server:call(?SERVER, {getpresence, From, Pid}).
%%

init([]) ->
    % set this so we can catch death of logged in pids:
    process_flag(trap_exit, true),
    {ok, C} = eredis:start_link(?REDIS_HOST, ?REDIS_PORT),
    Host = {?MONGO_HOST, ?MONGO_PORT},
    UcHost = {?UC_HOST, ?UC_PORT},
    {ok, MongoConn} = mongo:connect (Host),
    {ok, UcConn} = mongo:connect (UcHost),    
    % use ets for routing tables
    {ok, #state{
                sid2comet = ets:new(?MODULE, [set]),
                comet2sid =  ets:new(?MODULE, [set]),
                pid2id = ets:new(?MODULE, [bag]),
                id2pid = ets:new(?MODULE, [bag]),
                redisconn=C,
                mongoconn=MongoConn,
                ucconn = UcConn
               }
    }.

send_presence(C, To, Id, Tbl, Type) ->
    {ok, Result}= eredis:q(C, ["SISMEMBER", ?LOGINSET, To]),
    case Result of
        <<"1">> ->
            % get pids who are logged in as this Id
            Pids = [ P || { _Id, P } <- ets:lookup(Tbl, To) ],
            Stanza = {struct, [{<<"ns">>, <<"presence">>},{<<"type">>, Type },{<<"from">>, list_to_binary(Id)},{<<"to">>, list_to_binary(To)}]},
            % send Msg to them all
            M = {router_presence, Stanza},
	    [FirstPid |Any] = [Pids],
            io:format("send presence to pid : ~w\n",[FirstPid]),
            [ Pid ! M || Pid <- Pids ];    
	_ ->
            ok
    end.

handle_call({getpresence, Id, Pid}, _From, State) when is_pid(Pid) ->
    C = State#state.redisconn,
    eredis:q(C, ["SISMEMBER", ?LOGINSET, Id]),
    Fuids = get_subscribers(Id, State#state.ucconn),
    Online = lists:filter(fun(X)-> is_presence(C, X) end, Fuids),
    Result = lists:map(fun(F) -> {struct, [{<<"uid">>, F}]} end, Online),
    Pid ! {get_presence, Result},
    %[send_presence(C, Id, O, State#state.id2pid) ||O <- Fuids], 
    {reply, ok, State};

handle_call({login, {Sid, Id,Pwd}, Pid}, _From, State) when is_pid(Pid) ->
    C = State#state.redisconn,
    {ok, Result}= eredis:q(C, ["SISMEMBER", ?LOGINSET, Id]),
    Sid2Comet = State#state.sid2comet,
    Sid0 = Sid ++ ":" ++ Id, 
    case Result of
        <<"1">> ->
	    CometRows = ets:lookup(Sid2Comet, Sid0),
            case CometRows of
                [{Sid1, Comet1}] -> 
                    Comet1 ! {keep_alive, Pid};
                _  -> ok
            end,
            ok;
        _ ->       
            Q = fun() -> mongo:find_one(user, {'_id',list_to_binary(Id), password, list_to_binary(Pwd)}, {'_id', 1}) end,
            UcConn = State#state.ucconn,
            case mongo:do(safe, master, UcConn, usercenter, Q) of
	    {ok, {Doc}} ->
                 Comet = spawn(ttim_comet, init, [Pid]),
                 %ets:insert(State#state.pid2id, {Comet,Id}),
	    	 ets:insert(State#state.id2pid, {Id, Comet}),
                 link(Comet), % tell us if they exit, so we can log them out
	         io:format("~w logged in as ~s ~n",[Pid, Id]),
                 ets:insert(State#state.sid2comet, {Sid0, Comet}),  
                 ets:insert(State#state.comet2sid, {Comet, Sid0}),  
                 eredis:q(C, ["SADD", ?LOGINSET, Id]),
                 Stanza = {struct, [{<<"ns">>, <<"conn">>},{<<"result">>, <<"1">>}]},
                 M = {connected, Stanza},
                 Pid ! M;   
	    _ ->   
                 M = {failed_conn, "Invalid Username/Password"},
	         Pid ! M
         end
     end,  
  {reply, ok, State};

handle_call({logout, Pid}, _From, State) when is_pid(Pid) ->
    unlink(Pid),
    PidRows = ets:lookup(State#state.comet2sid, Pid),
    MongoConn = State#state.mongoconn,
    case PidRows of
        [] ->
            ok;
        _ ->
            IdRows = [ {I,P} || {P,I} <- PidRows ], % invert tuples
            Sids = [I|| {I,P} <- IdRows],
            Ids = [string:sub_string(I, string:chr(I, $:) + 1) || {I,P} <- IdRows],
	    % delete all pid->id entries
            ets:delete(State#state.comet2sid, Pid),
            % and all id->pid
            [ ets:delete_object(State#state.id2pid, {Obj, Pid}) || Obj <- Ids ],
            [ ets:delete(State#state.sid2comet, Obj) || Obj <- Sids],
            [Uid | _] = Ids,
            Subs = get_subscribers(Uid, MongoConn),
            Subs0 = sets:to_list(sets:from_list(Subs)),
            C = State#state.redisconn,
            lists:foreach(fun(X) -> send_presence(C, X, Uid, State#state.id2pid, <<"2">>) end, Subs0),
            eredis:q(C, ["SREM", ?LOGINSET | Ids])
    end,
    io:format("pid ~w logged out\n",[Pid]),
    {reply, ok, State};

handle_call({send, Id, From, Msg}, _From, State) ->
    %C = State#state.redisconn,
    %eredis:q(C, ["LPUSH", Id+".msg", Msg]),
    % get pids who are logged in as this Id
    Pids = [ P || { _Id, P } <- ets:lookup(State#state.id2pid, Id) ],
    Stanza = {struct, [{<<"ns">>, <<"message">>},{<<"from">>, list_to_binary(From)},{<<"to">>, list_to_binary(Id)},{<<"message">>, list_to_binary(Msg)}]},
    % send Msg to them all
    M = {send_msg, Stanza},
    [FirstPid |Any] = [Pids],
    io:format("first pid : ~w\n",[FirstPid]),
    [ Pid ! M || Pid <- Pids ],
    {reply, ok, State}.

% handle death and cleanup of logged in processes
handle_info(Info, State) ->
    case Info of
        {'EXIT', Pid, Why} ->
            io:format("why: ~p ~n", [Why]),
            % force logout:
            handle_call({logout, Pid}, blah, State); 
        Wtf ->
            io:format("Caught unhandled message: ~w ~n", [Wtf])
    end,
    {noreply, State}.

handle_cast(_Msg, State) ->
    {noreply, State}.
terminate(_Reason, _State) ->
    ok.
code_change(_OldVsn, State, _Extra) ->
    {ok, State}.

get_subscribers(Uid, MongoConn) ->
    Coll = list_to_atom("CTL_USER_GROUP"),    
    Q = fun() -> mongo:find_one(Coll, {'_id',list_to_binary(Uid)},{'groups', 1}) end,   
    case mongo:do(safe, master, MongoConn, tt_group, Q) of 
        {ok, {Doc}} ->    
	    {Groups} = bson:lookup(groups, Doc),
	    Gids = get_odd(tuple_to_list(Groups), []),
            Q1 = fun() -> mongo:rest(mongo:find('CLT_GROUP_MEMBER', {'gid',{'$in', Gids}, status, <<"2">>}, {uid, 1})) end,
            case mongo:do(safe, master, MongoConn, tt_group, Q1) of
                {ok, SubsObj} ->
		    lists:flatmap(fun(X)-> [element(4,X)] end, SubsObj);
		_ ->
                    []
            end;
	_ ->
            []
     end.

is_presence(C,Id) ->
    {ok, Result}= eredis:q(C, ["SISMEMBER", ?LOGINSET, Id]),
    case Result of
        <<"1">> ->
           true;
	_ ->
           false
    end.	    

get_odd([], Result) ->
    Result;
get_odd([First, Second| Rest], Result) ->
    get_odd(Rest, [list_to_binary(atom_to_list(First)) | Result]).

comet_loop(Req)->
    receive
    {keep_alive, NewReq} ->
        comet_loop(NewReq);
    {send_msg, Msg} ->
        Req ! {new_msg, Msg},
        comet_loop(Req) 
    after 20000 ->
        ok
    end.    
